/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.validator.engine;

import java.beans.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import ch.trackedbean.business.*;
import ch.trackedbean.copier.annotations.*;
import ch.trackedbean.util.*;
import ch.trackedbean.validator.*;
import ch.trackedbean.validator.annotations.*;
import ch.trackedbean.validator.businessValidator.*;
import ch.trackedbean.validator.engine.ExternalAnnotationMapping.*;

/**
 * Helper object for building an {@link Validator} based on {@link BeanPropertyValidator} annotations and mapping annotations ({@link SimpleMapping},
 * {@link PathMapping} etc.).
 * 
 * @author M. Hautle
 */
public class DefaultValidatorConfigurator {
	/** The validator to build. */
	protected final Validator validator;

	/** The type for which the validator is built. */
	protected final Class<?> type;

	/** The base class specified in {@link SourceClass} (if the validator inheritance is enabled) or null. */
	protected final Class<?> mappedBaseType;

	/** Configuration holder. This holder will be reused for each property validator configuration. */
	protected final Configuration cfg = new Configuration();

	/** Set holding the property names of properties were no default mapping validator has to be registred. */
	protected final Set<String> noDefaultMappingValidation = new HashSet<String>();

	/**
	 * Default constructor.
	 * 
	 * @param type The type for which the validator is built
	 */
	public DefaultValidatorConfigurator(Class<?> type) {
		this.validator = new Validator(type);
		this.type = type;
		mappedBaseType = getMappedBaseClass(type);
	}

	/**
	 * Returns the mapped base class for validation inheritance.
	 * 
	 * @param type The type
	 * @return The mapped base class
	 */
	protected Class<?> getMappedBaseClass(Class<?> type) {
		final SourceClass src = type.getAnnotation(SourceClass.class);
		// no mapped class
		if (src == null || !src.inheritValidation())
			return null;
		final Class<?> base = src.value();
		return ValidatorManager.getValidator(base).doesSomething() ? base : null;
	}

	/**
	 * Creates and configures the validator.
	 * 
	 * @return The created validator
	 * @throws ValidatorInitialisationException If something went wrong
	 */
	public IValidator create() throws ValidatorInitialisationException {
		try {
			inspectFields();
			inspectPropertyMethods();
			appendParentValidations();
			appendContentValidation();
			appendBusinessContentValidation();
			cleanUp();
			return validator;
		} catch (InstantiationException e) {
			throw new ValidatorInitialisationException(e);
		} catch (IllegalAccessException e) {
			throw new ValidatorInitialisationException(e);
		} catch (IntrospectionException e) {
			throw new ValidatorInitialisationException(e);
		}
	}

	/**
	 * Inspects the fields of the given class and registers the declared validators.
	 * 
	 * @throws InstantiationException If something went wrong
	 * @throws IllegalAccessException If something went wrong
	 */
	protected void inspectFields() throws InstantiationException, IllegalAccessException {
		// inspect the fields declared in this class
		for (Field f : type.getDeclaredFields())
			inspectAnnotations(f.getName(), f);
	}

	/**
	 * Inspects the properties of the given class and registers the declared validators.
	 * 
	 * @throws IntrospectionException If something went wrong
	 * @throws InstantiationException If something went wrong
	 * @throws IllegalAccessException If something went wrong
	 */
	protected void inspectPropertyMethods() throws IntrospectionException, InstantiationException, IllegalAccessException {
		final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
		if (props == null)
			return;
		for (PropertyDescriptor d : props) {
			final Method read = d.getReadMethod();
			// skip properties not declared in the current class
			if (read == null || type != read.getDeclaringClass())
				continue;
			final String name = d.getName();
			inspectAnnotations(name, read);
			addInheritedMappingValidators(name);
		}
	}

	/**
	 * Appends the validation information from the parent class.
	 */
	protected void appendParentValidations() {
		final IValidator val = ValidatorManager.getValidator(type.getSuperclass());
		if (val.doesSomething() && val instanceof Validator)
			validator.appendValidatorInformation((Validator) val);
	}

	/**
	 * Appends {@link IBusinessValidator}s defined by {@link ValidateContent}/{@link ValidateBusinessContents}.
	 * 
	 * @throws IllegalAccessException If something went wrong
	 * @throws InstantiationException If something went wrong
	 */
	protected void appendContentValidation() throws InstantiationException, IllegalAccessException {
		final ValidateContent c = type.getAnnotation(ValidateContent.class);
		if (c != null)
			addContentValidator(c.value());
		final ValidateContents cs = type.getAnnotation(ValidateContents.class);
		if (cs != null)
			for (ValidateContent i : cs.value())
				addContentValidator(i.value());
	}

	/**
	 * Creates a {@link IBusinessValidator} from the given type and adds it to the validator.
	 * 
	 * @param type The validator class
	 * @throws InstantiationException If something went wrong
	 * @throws IllegalAccessException If something went wrong
	 */
	protected void addContentValidator(Class<? extends IBusinessValidator> type) throws InstantiationException, IllegalAccessException {
		validator.addBusinessValidator(type.newInstance());
	}

	/**
	 * Appends {@link AbstractBusinessValidator}s defined by {@link ValidateBusinessContent}/{@link ValidateBusinessContents}.
	 * 
	 * @throws InstantiationException If something went wrong
	 * @throws IllegalAccessException If something went wrong
	 */
	protected void appendBusinessContentValidation() throws InstantiationException, IllegalAccessException {
		final ValidateBusinessContent c = type.getAnnotation(ValidateBusinessContent.class);
		if (c != null)
			addBusinessContentValidator(c.value(), c.resolver(), c.baseClass());
		final ValidateBusinessContents cs = type.getAnnotation(ValidateBusinessContents.class);
		if (cs != null)
			for (ValidateBusinessContent i : cs.value())
				addBusinessContentValidator(i.value(), i.resolver(), i.baseClass());
	}

	/**
	 * Creates and configures a {@link AbstractBusinessValidator} from the given type and adds it to the validator.
	 * 
	 * @param type The validator class
	 * @param resolver The class of the resolver to use
	 * @param baseClass The base class to use if the resolver is a {@link MappingResolver}
	 * @throws InstantiationException If something went wrong
	 * @throws IllegalAccessException If something went wrong
	 */
	protected void addBusinessContentValidator(Class<? extends AbstractBusinessValidator> type, Class<? extends IPropertyResolver> resolver, Class baseClass)
			throws InstantiationException, IllegalAccessException {
		final AbstractBusinessValidator v = type.newInstance();
		final IPropertyResolver res = resolver.newInstance();
		if (Object.class != baseClass && res instanceof MappingResolver)
			((MappingResolver) res).addMappedProperties(baseClass);
		v.setResolver(res);
		validator.addBusinessValidator(v);
	}

	/**
	 * Inspects the annotations of the given element and registers the declared validators.
	 * 
	 * @param name The property name
	 * @param el The annotated element
	 * @throws IllegalAccessException If something went wrong
	 * @throws InstantiationException If something went wrong
	 */
	protected void inspectAnnotations(String name, AnnotatedElement el) throws InstantiationException, IllegalAccessException {
		inspectMappingValidation(name, el);
		addDeepValidation(name, el);
		for (Annotation a : el.getAnnotations()) {
			final IPropertyValidator val = createValidator(a);
			if (val != null)
				validator.addPropertyValidation(name, val);
		}
	}

	/**
	 * Creates a validator for the given annotation if it's a validation annotation.
	 * 
	 * @param a The annotation
	 * @return The corresponding validator or null
	 */
	protected IPropertyValidator createValidator(Annotation a) {
		final Class<? extends Annotation> type = a.annotationType();
		final ExternalAnnotationMapping extern = ValidatorManager.getMappedValidator(type);
		if (extern != null)
			return createValidator(extern, a);
		final BeanPropertyValidator intern = type.getAnnotation(BeanPropertyValidator.class);
		if (intern != null)
			return createValidator(intern.value(), a);
		return null;
	}

	/**
	 * Creates a validator for the given external annotation.
	 * 
	 * @param extern The external mapping
	 * @param a The annotation holding the configuration
	 * @return The validator
	 * @throws ValidatorInitialisationException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	protected IPropertyValidator createValidator(ExternalAnnotationMapping extern, Annotation a) {
		try {
			extern.copyConfig(a, cfg);
			final Class<? extends IPropertyValidator> type = extern.getValidatorType();
			final IPropertyValidator val = type.newInstance();
			val.init(cfg);
			return val;
		} catch (IllegalAccessException e) {
			throw new ValidatorInitialisationException(e);
		} catch (InstantiationException e) {
			throw new ValidatorInitialisationException(e);
		} catch (ExternalConfigurationException e) {
			throw new ValidatorInitialisationException(e);
		}
	}

	/**
	 * Creates an configures the validator for the given annotation.
	 * 
	 * @param type The validator type
	 * @param a The annotation holding the configuration
	 * @return The validator
	 * @throws ValidatorInitialisationException If something went wrong
	 */
	protected IPropertyValidator createValidator(Class<? extends IPropertyValidator> type, Annotation a) throws ValidatorInitialisationException {
		try {
			cfg.clear();
			for (Method m : a.annotationType().getDeclaredMethods())
				cfg.addEntry(m.getName(), m.invoke(a));
			final IPropertyValidator val = type.newInstance();
			val.init(cfg);
			return val;
		} catch (IllegalAccessException e) {
			throw new ValidatorInitialisationException(e);
		} catch (InvocationTargetException e) {
			throw new ValidatorInitialisationException(e);
		} catch (InstantiationException e) {
			throw new ValidatorInitialisationException(e);
		}
	}

	/**
	 * Adds a deep validation if the given element is annotated with {@link DeepValidation}.
	 * 
	 * @param name The property name
	 * @param el The annotated element
	 */
	protected void addDeepValidation(String name, AnnotatedElement el) {
		final DeepValidation deep = el.getAnnotation(DeepValidation.class);
		if (deep != null)
			validator.addDeepValidation(name, deep.message());
	}

	/**
	 * Inspects mapping annotation and extracts validation relevant information.
	 * 
	 * @param property The property name
	 * @param el The annotated element
	 */
	protected void inspectMappingValidation(String property, AnnotatedElement el) {
		if (ignoreInheritedMappingValidation(property, el))
			return;
		final PathMapping path = el.getAnnotation(PathMapping.class);
		if (path != null)
			addPathMapping(property, path);
		final SimpleMapping simple = el.getAnnotation(SimpleMapping.class);
		if (simple != null)
			addSimpleMapping(property, simple);
	}

	/**
	 * Checks if the given element should not inherit it's validation rules throug mapping annotations.<br>
	 * Ensures that a call of {@link #addInheritedMappingValidators(String)} does nothig if not desired.
	 * 
	 * @param name The property name
	 * @param el The property element
	 * @return True if the element should not inherit it's validation rules
	 */
	protected boolean ignoreInheritedMappingValidation(String name, AnnotatedElement el) {
		final boolean ignore = el.getAnnotation(IgnoreInheritedValidation.class) != null || el.getAnnotation(IgnoreMapping.class) != null;
		if (ignore)
			noDefaultMappingValidation.add(name);
		return ignore;
	}

	/**
	 * Adds the necessary validators for the given property name based on the defined {@link SimpleMapping}.
	 * 
	 * @param srcProperty The name of the property in the source type (from which the validators were inherited)
	 * @param simple The mapping annotation
	 */
	protected void addSimpleMapping(String srcProperty, SimpleMapping simple) {
		noDefaultMappingValidation.add(srcProperty);
		final Class<?> srcClass = simple.srcClass();
		final Class<?> type = srcClass != Object.class ? srcClass : mappedBaseType;
		if (type != null && simple.inheritValidation())
			inheritValidators(srcProperty, type, simple.value());
	}

	/**
	 * Adds the necessary validators for the given {@link PathMapping} annotation.
	 * 
	 * @param property The property
	 * @param mapping The mapping annotation
	 */
	protected void addPathMapping(String property, PathMapping mapping) {
		noDefaultMappingValidation.add(property);
		final Class<?> lastType = mapping.lastPathType();
		if (lastType != Object.class)
			inheritValidators(property, lastType, ELHelper.getLastProperty(mapping.path()));
	}

	/**
	 * Adds the validators of the default mapping of the given property.<br>
	 * This is only done if no other mapping annotation exists on the specified property!
	 * 
	 * @param property The property
	 */
	protected void addInheritedMappingValidators(String property) {
		if (mappedBaseType != null && noDefaultMappingValidation.add(property))
			inheritValidators(property, mappedBaseType, property);
	}

	/**
	 * Adds all validators registred on the specified source property in the specified type to the validator list of the passed property.
	 * 
	 * @param dstProperty The property name in the type for which the validator is built
	 * @param src The source type (from which the validators were inherited)
	 * @param srcProperty The name of the property in the source type
	 */
	protected void inheritValidators(String dstProperty, Class<?> src, String srcProperty) {
		final IValidator val = ValidatorManager.getValidator(src);
		if (val instanceof Validator)
			validator.appendSimpleValidators((Validator) val, srcProperty, dstProperty);
	}

	/**
	 * Cleans up the helper.
	 */
	protected void cleanUp() {
		noDefaultMappingValidation.clear();
	}
}